// Copyright Epic Games, Inc. All Rights Reserved.

#include "copy_cmd.h"

#include <zencore/filesystem.h>
#include <zencore/fmtutils.h>
#include <zencore/logging.h>
#include <zencore/string.h>
#include <zencore/timer.h>

namespace zen {

CopyCommand::CopyCommand()
{
	m_Options.add_options()("h,help", "Print help");
	m_Options.add_options()("no-clone", "Do not perform block clone", cxxopts::value(m_NoClone)->default_value("false"));
	m_Options.add_options()("must-clone",
							"Always perform block clone (fails if clone is not possible)",
							cxxopts::value(m_MustClone)->default_value("false"));
	m_Options.add_option("", "s", "source", "Copy source", cxxopts::value(m_CopySource), "<file/directory>");
	m_Options.add_option("", "t", "target", "Copy target", cxxopts::value(m_CopyTarget), "<file/directory>");
	m_Options.parse_positional({"source", "target"});
}

CopyCommand::~CopyCommand() = default;

int
CopyCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
	ZEN_UNUSED(GlobalOptions);

	if (!ZenCmdBase::ParseOptions(argc, argv))
	{
		return 0;
	}

	// Validate arguments

	if (m_CopySource.empty())
		throw std::runtime_error("No source specified");

	if (m_CopyTarget.empty())
		throw std::runtime_error("No target specified");

	std::filesystem::path FromPath;
	std::filesystem::path ToPath;

	FromPath = m_CopySource;
	ToPath	 = m_CopyTarget;

	std::error_code		  Ec;
	std::filesystem::path FromCanonical = std::filesystem::canonical(FromPath, Ec);

	if (!Ec)
	{
		std::filesystem::path ToCanonical = std::filesystem::canonical(ToPath, Ec);

		if (!Ec)
		{
			if (FromCanonical == ToCanonical)
			{
				throw std::runtime_error("Target and source must be distinct files or directories");
			}
		}
	}

	const bool IsFileCopy = std::filesystem::is_regular_file(m_CopySource);
	const bool IsDirCopy  = std::filesystem::is_directory(m_CopySource);

	if (!IsFileCopy && !IsDirCopy)
	{
		throw std::runtime_error("Invalid source specification (neither directory nor file)");
	}

	if (IsFileCopy && IsDirCopy)
	{
		throw std::runtime_error("Invalid source specification (both directory AND file!?)");
	}

	if (IsDirCopy)
	{
		if (std::filesystem::exists(ToPath))
		{
			const bool IsTargetDir = std::filesystem::is_directory(ToPath);
			if (!IsTargetDir)
			{
				if (std::filesystem::is_regular_file(ToPath))
				{
					throw std::runtime_error("Attempted copy of directory into file");
				}
			}
		}
		else
		{
			std::filesystem::create_directories(ToPath);
		}

		std::filesystem::path ToCanonical = std::filesystem::canonical(ToPath, Ec);

		if (!Ec)
		{
			if (ToCanonical.generic_string().starts_with(FromCanonical.generic_string()) ||
				FromCanonical.generic_string().starts_with(ToCanonical.generic_string()))
			{
				throw std::runtime_error("Invalid parent/child relationship for source/target directories");
			}
		}

		// Multi file copy

		ZEN_CONSOLE("copying {} -> {}", FromPath, ToPath);

		zen::Stopwatch Timer;

		struct CopyVisitor : public FileSystemTraversal::TreeVisitor
		{
			CopyVisitor(std::filesystem::path InBasePath, zen::CopyFileOptions InCopyOptions)
			: BasePath(InBasePath)
			, CopyOptions(InCopyOptions)
			{
			}

			virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t FileSize) override
			{
				ZEN_UNUSED(FileSize);
				std::error_code				Ec;
				const std::filesystem::path Relative = std::filesystem::relative(Parent, BasePath, Ec);

				if (Ec)
				{
					FailedFileCount++;
				}
				else
				{
					const std::filesystem::path FromPath = Parent / File;
					const std::filesystem::path ToPath	 = TargetPath / Relative / File;

					try
					{
						zen::CreateDirectories(TargetPath / Relative);
						if (zen::CopyFile(FromPath, ToPath, CopyOptions))
						{
							++FileCount;
							ByteCount += FileSize;
						}
						else
						{
							throw std::logic_error("CopyFile failed in an unexpected way");
						}
					}
					catch (const std::exception& Ex)
					{
						++FailedFileCount;

						ZEN_CONSOLE_ERROR("failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what());
					}
				}
			}

			virtual bool VisitDirectory(const std::filesystem::path&, const path_view&) override { return true; }

			std::filesystem::path BasePath;
			std::filesystem::path TargetPath;
			zen::CopyFileOptions  CopyOptions;
			int					  FileCount		  = 0;
			uint64_t			  ByteCount		  = 0;
			int					  FailedFileCount = 0;
		};

		zen::CopyFileOptions CopyOptions;
		CopyOptions.EnableClone = !m_NoClone;
		CopyOptions.MustClone	= m_MustClone;

		CopyVisitor Visitor{FromPath, CopyOptions};
		Visitor.TargetPath = ToPath;

		FileSystemTraversal Traversal;
		Traversal.TraverseFileSystem(FromPath, Visitor);

		ZEN_CONSOLE("Copy of {} files ({}) completed in {} ({})",
					Visitor.FileCount,
					NiceBytes(Visitor.ByteCount),
					zen::NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
					zen::NiceRate(Visitor.ByteCount, (uint32_t)Timer.GetElapsedTimeMs()));

		if (Visitor.FailedFileCount)
		{
			ZEN_CONSOLE_ERROR("{} file copy operations FAILED", Visitor.FailedFileCount);

			return 1;
		}
	}
	else
	{
		// Single file copy

		zen::Stopwatch Timer;

		zen::CopyFileOptions CopyOptions;
		CopyOptions.EnableClone = !m_NoClone;

		try
		{
			zen::CreateDirectories(ToPath.parent_path());
			if (zen::CopyFile(FromPath, ToPath, CopyOptions))
			{
				ZEN_CONSOLE("Copy completed in {}", zen::NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
			}
			else
			{
				throw std::logic_error("CopyFile failed in an unexpected way");
			}
		}
		catch (const std::exception& Ex)
		{
			ZEN_CONSOLE_ERROR("Error: failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what());

			return 1;
		}
	}

	return 0;
}

}  // namespace zen
